package org.andengine.opengl.font;

import org.andengine.entity.text.AutoWrap;
import org.andengine.util.TextUtils;
import org.andengine.util.exception.MethodNotYetImplementedException;

import java.util.List;


/**
 * (c) Zynga 2012
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 15:06:32 - 25.01.2012
 */
public class FontUtils {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final int UNSPECIFIED = -1;

    // ===========================================================
    // Fields
    // ===========================================================

    // ===========================================================
    // Constructors
    // ===========================================================

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    // ===========================================================
    // Methods
    // ===========================================================

    /**
     * @param pFont
     * @param pText
     * @return the width of pText.
     */
    public static float measureText(final IFont pFont, final CharSequence pText) {
        return FontUtils.measureText(pFont, pText, null);
    }

    /**
     * @param pFont
     * @param pText
     * @param pStart the index of the first character to start measuring.
     * @param pEnd   <code>1</code> beyond the index of the last character to measure.
     * @return the width of pText.
     */
    public static float measureText(final IFont pFont, final CharSequence pText, final int pStart, final int pEnd) {
        return FontUtils.measureText(pFont, pText, pStart, pEnd, null);
    }

    /**
     * @param pFont
     * @param pText
     * @param pWidths (optional) If not <code>null</code>, returns the actual width measured.
     * @return the width of pText.
     */
    public static float measureText(final IFont pFont, final CharSequence pText, final float[] pWidths) {
        return FontUtils.measureText(pFont, pText, 0, pText.length(), pWidths);
    }

    /**
     * Does not respect linebreaks!
     *
     * @param pFont
     * @param pText
     * @param pStart  the index of the first character to start measuring.
     * @param pEnd    <code>1</code> beyond the index of the last character to measure.
     * @param pWidths (optional) If not <code>null</code>, returns the actual width after each character.
     * @return the width of pText.
     */
    public static float measureText(final IFont pFont, final CharSequence pText, final int pStart, final int pEnd, final float[] pWidths) {
        final int textLength = pEnd - pStart;
        /* Early exits. */
        if (pStart == pEnd) {
            return 0;
        } else if (textLength == 1) {
            return pFont.getLetter(pText.charAt(pStart)).mWidth;
        }

        Letter previousLetter = null;
        float width = 0;
        for (int pos = pStart, i = 0; pos < pEnd; pos++, i++) {
            final Letter letter = pFont.getLetter(pText.charAt(pos));
            if (previousLetter != null) {
                width += previousLetter.getKerning(letter.mCharacter);
            }
            previousLetter = letter;

			/* Check if this is the last character. */
            if (pos == (pEnd - 1)) {
                width += letter.mOffsetX + letter.mWidth;
            } else {
                width += letter.mAdvance;
            }

            if (pWidths != null) {
                pWidths[i] = width;
            }
        }
        return width;
    }

    /**
     * Measure the text, stopping early if the measured width exceeds pMaximumWidth.
     *
     * @param pFont
     * @param pText
     * @param pMeasureDirection If {@link MeasureDirection#FORWARDS}, starts with the first character in the {@link CharSequence}. If {@link MeasureDirection#BACKWARDS} starts with the last character in the {@link CharSequence}.
     * @param pWidthMaximum
     * @param pMeasuredWidth    (optional) If not <code>null</code>, returns the actual width measured. Must be an Array of size <code>1</code> or bigger.
     * @return the number of chars that were measured.
     */
    public static int breakText(final IFont pFont, final CharSequence pText, final MeasureDirection pMeasureDirection, final float pWidthMaximum, final float[] pMeasuredWidth) {
        throw new MethodNotYetImplementedException();
    }

    public static <L extends List<CharSequence>> L splitLines(final CharSequence pText, final L pResult) {
        return TextUtils.split(pText, '\n', pResult);
    }

    /**
     * Does not respect linebreaks!
     *
     * @param pFont
     * @param pText
     * @param pResult
     * @param pAutoWrapWidth
     * @return
     */
    public static <L extends List<CharSequence>> L splitLines(final IFont pFont, final CharSequence pText, final L pResult, final AutoWrap pAutoWrap, final float pAutoWrapWidth) {
        /**
         * TODO In order to respect already existing linebreaks, {@link FontUtils#split(CharSequence, List)} could be leveraged and than the following methods could be called for each line.
         */
        switch (pAutoWrap) {
            case LETTERS:
                return FontUtils.splitLinesByLetters(pFont, pText, pResult, pAutoWrapWidth);
            case WORDS:
                return FontUtils.splitLinesByWords(pFont, pText, pResult, pAutoWrapWidth);
            case CJK:
                return FontUtils.splitLinesByCJK(pFont, pText, pResult, pAutoWrapWidth);
            case NONE:
            default:
                throw new IllegalArgumentException("Unexpected " + AutoWrap.class.getSimpleName() + ": '" + pAutoWrap + "'.");
        }
    }

    private static <L extends List<CharSequence>> L splitLinesByLetters(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
        final int textLength = pText.length();

        int lineStart = 0;
        int lineEnd = 0;
        int lastNonWhitespace = 0;
        boolean charsAvailable = false;

        for (int i = 0; i < textLength; i++) {
            final char character = pText.charAt(i);
            if (character != ' ') {
                if (charsAvailable) {
                    lastNonWhitespace = i + 1;
                } else {
                    charsAvailable = true;
                    lineStart = i;
                    lastNonWhitespace = lineStart + 1;
                    lineEnd = lastNonWhitespace;
                }
            }

            if (charsAvailable) {
//				/* Just for debugging. */
//				final CharSequence line = pText.subSequence(lineStart, lineEnd);
//				final float lineWidth = FontUtils.measureText(pFont, pText, lineStart, lineEnd);
//
//				final CharSequence lookaheadLine = pText.subSequence(lineStart, lastNonWhitespace);
                final float lookaheadLineWidth = FontUtils.measureText(pFont, pText, lineStart, lastNonWhitespace);

                final boolean isEndReached = (i == (textLength - 1));
                if (isEndReached) {
                    /* When the end of the string is reached, add remainder to result. */
                    if (lookaheadLineWidth <= pAutoWrapWidth) {
                        pResult.add(pText.subSequence(lineStart, lastNonWhitespace));
                    } else {
                        pResult.add(pText.subSequence(lineStart, lineEnd));
                        /* Avoid special case where last line is added twice. */
                        if (lineStart != i) {
                            pResult.add(pText.subSequence(i, lastNonWhitespace));
                        }
                    }
                } else {
                    if (lookaheadLineWidth <= pAutoWrapWidth) {
                        lineEnd = lastNonWhitespace;
                    } else {
                        pResult.add(pText.subSequence(lineStart, lineEnd));
                        i = lineEnd - 1;
                        charsAvailable = false;
                    }
                }
            }
        }

        return pResult;
    }

    private static <L extends List<CharSequence>> L splitLinesByWords(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
        final int textLength = pText.length();

        if (textLength == 0) {
            return pResult;
        }

        final float spaceWidth = pFont.getLetter(' ').mAdvance;

        int lastWordEnd = FontUtils.UNSPECIFIED;
        int lineStart = FontUtils.UNSPECIFIED;
        int lineEnd = FontUtils.UNSPECIFIED;

        float lineWidthRemaining = pAutoWrapWidth;
        boolean firstWordInLine = true;
        int i = 0;
        while (i < textLength) {
            int spacesSkipped = 0;
			/* Find next word. */
            { /* Skip whitespaces. */
                while ((i < textLength) && (pText.charAt(i) == ' ')) {
                    i++;
                    spacesSkipped++;
                }
            }
            final int wordStart = i;

			/* Mark beginning of a new line. */
            if (lineStart == FontUtils.UNSPECIFIED) {
                lineStart = wordStart;
            }

            { /* Skip non-whitespaces. */
                while ((i < textLength) && (pText.charAt(i) != ' ')) {
                    i++;
                }
            }
            final int wordEnd = i;

			/* Nothing more could be read. */
            if (wordStart == wordEnd) {
                if (!firstWordInLine) {
                    pResult.add(pText.subSequence(lineStart, lineEnd));
                }
                break;
            }

//			/* Just for debugging. */
//			final CharSequence word = pText.subSequence(wordStart, wordEnd);

            final float wordWidth = FontUtils.measureText(pFont, pText, wordStart, wordEnd);

			/* Determine the width actually needed for the current word. */
            final float widthNeeded;
            if (firstWordInLine) {
                widthNeeded = wordWidth;
            } else {
                widthNeeded = (spacesSkipped * spaceWidth) + wordWidth;
            }

			/* Check if the word fits into the rest of the line. */
            if (widthNeeded <= lineWidthRemaining) {
                if (firstWordInLine) {
                    firstWordInLine = false;
                } else {
                    lineWidthRemaining -= FontUtils.getAdvanceCorrection(pFont, pText, lastWordEnd - 1);
                }
                lineWidthRemaining -= widthNeeded;
                lastWordEnd = wordEnd;
                lineEnd = wordEnd;

				/* Check if the end was reached. */
                if (wordEnd == textLength) {
                    pResult.add(pText.subSequence(lineStart, lineEnd));
					/* Added the last line. */
                    break;
                }
            } else {
				/* Special case for lines with only one word. */
                if (firstWordInLine) {
					/* Check for lines that are just too big. */
                    if (wordWidth >= pAutoWrapWidth) {
                        pResult.add(pText.subSequence(wordStart, wordEnd));
                        lineWidthRemaining = pAutoWrapWidth;
                    } else {
                        lineWidthRemaining = pAutoWrapWidth - wordWidth;

						/* Check if the end was reached. */
                        if (wordEnd == textLength) {
                            pResult.add(pText.subSequence(wordStart, wordEnd));
							/* Added the last line. */
                            break;
                        }
                    }

					/* Start a completely new line. */
                    firstWordInLine = true;
                    lastWordEnd = FontUtils.UNSPECIFIED;
                    lineStart = FontUtils.UNSPECIFIED;
                    lineEnd = FontUtils.UNSPECIFIED;
                } else {
					/* Finish the current line. */
                    pResult.add(pText.subSequence(lineStart, lineEnd));

					/* Check if the end was reached. */
                    if (wordEnd == textLength) {
						/* Add the last word. */
                        pResult.add(pText.subSequence(wordStart, wordEnd)); // TODO Does this cover all cases?
                        break;
                    } else {
						/* Start a new line, carrying over the current word. */
                        lineWidthRemaining = pAutoWrapWidth - wordWidth;
                        firstWordInLine = false;
                        lastWordEnd = wordEnd;
                        lineStart = wordStart;
                        lineEnd = wordEnd;
                    }
                }
            }
        }
        return pResult;
    }

    private static <L extends List<CharSequence>> L splitLinesByCJK(final IFont pFont, final CharSequence pText, final L pResult, final float pAutoWrapWidth) {
        final int textLength = pText.length();

        int lineStart = 0;
        int lineEnd = 0;

		/* Skip whitespaces at the beginning of the string. */
        while ((lineStart < textLength) && (pText.charAt(lineStart) == ' ')) {
            lineStart++;
            lineEnd++;
        }

        int i = lineEnd;
        while (i < textLength) {
            lineStart = lineEnd;

            { /* Look for a sub string */
                boolean charsAvailable = true;
                while (i < textLength) {

                    { /* Skip whitespaces at the end of the string */
                        int j = lineEnd;
                        while (j < textLength) {
                            if (pText.charAt(j) == ' ') {
                                j++;
                            } else {
                                break;
                            }
                        }
                        if (j == textLength) {
                            if (lineStart == lineEnd) {
                                charsAvailable = false;
                            }
                            i = textLength;
                            break;
                        }
                    }

                    lineEnd++;

                    final float lineWidth = FontUtils.measureText(pFont, pText, lineStart, lineEnd);

                    if (lineWidth > pAutoWrapWidth) {
                        if (lineStart < lineEnd - 1) {
                            lineEnd--;
                        }

                        pResult.add(pText.subSequence(lineStart, lineEnd));
                        charsAvailable = false;
                        i = lineEnd;
                        break;
                    }
                    i = lineEnd;
                }

                if (charsAvailable) {
                    pResult.add(pText.subSequence(lineStart, lineEnd));
                }
            }
        }

        return pResult;
    }

    private static float getAdvanceCorrection(final IFont pFont, final CharSequence pText, final int pIndex) {
        final Letter lastWordLastLetter = pFont.getLetter(pText.charAt(pIndex));
        return -(lastWordLastLetter.mOffsetX + lastWordLastLetter.mWidth) + lastWordLastLetter.mAdvance;
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public enum MeasureDirection {
        // ===========================================================
        // Elements
        // ===========================================================

        FORWARDS,
        BACKWARDS;

        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        // ===========================================================
        // Constructors
        // ===========================================================

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
